﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Snowdreamist.Collection.Tree;
using Snowdreamist.Helpers;

namespace Snowdreamist.Text.UBB
{
    public abstract class UBBReader
    {
        public const int MaxLabelLength = 16;

        #region static

        public static UBBReader Create(UBBParseStrategy strategy)
        {
            return Create(strategy, new PrefixAggregateUBBElementFactory());
        }

        public static UBBReader Create(UBBParseStrategy strategy, IUBBElementFactory ubbElementFactory)
        {
            switch (strategy)
            {
                case UBBParseStrategy.NoGreedy:
                    return new NoGreedyUBBReader(ubbElementFactory);

                default:
                    throw new ArgumentException("Unknown strategy");
            }
        }

        #endregion static

        public UBBReader(IUBBElementFactory elmFactory)
        {
            if (elmFactory == null)
                throw new ArgumentNullException("tagFactory");
            this.ElementFactory = elmFactory;
        }

        /// <summary>
        /// The parsing strategy
        /// </summary>
        public abstract UBBParseStrategy Strategy { get; }

        /// <summary>
        /// Load UBB document
        /// </summary>
        /// <returns></returns>
        public virtual UBBDocument Load(string ubbText)
        {
            if (ubbText == null)
                throw new ArgumentNullException("ubbText");
            return this.BuildDocument(
                this.MatchUBBLabels(
                    this.SplitUBBContents(ubbText)
                    )
                );
        }

        #region protected

        protected virtual IEnumerable<UBBContent> SplitUBBContents(string ubbText)
        {
            if (ubbText == null)
                throw new ArgumentNullException("ubbText");
            // the index indicates the current testing position
            int testIndex = 0;
            // the index indicates the current read start position
            int readIndex = 0;

            while (testIndex < ubbText.Length)
            {
                // find left bracket
                var leftBracketIndex = this.FindNextLeftBracket(ubbText, testIndex);
                if (leftBracketIndex == -1) // no left bracket found
                    break;
                // move test index
                testIndex = leftBracketIndex + 1;
                // find right bracket
                var rightBracketIndex = this.FindNextRightBracket(ubbText, testIndex, UBBReader.MaxLabelLength);
                if (rightBracketIndex == -1)    // no right bracket found
                {
                    // read as text
                    var len = ubbText.Length - readIndex < UBBReader.MaxLabelLength ? ubbText.Length - readIndex : UBBReader.MaxLabelLength;
                    yield return new UBBContent(ubbText.Substring(readIndex, len));
                    // move index
                    readIndex += len;
                    testIndex += len;
                    continue;
                }
                // find text PROBABLY a label, test if the text surrounded by [ and ] is a label
                IUBBElementFactory factory = null;
                if (this.IsLabel(ubbText.Substring(leftBracketIndex + 1, rightBracketIndex - leftBracketIndex - 1), out factory))
                {
                    // yes a label
                    // move test index
                    testIndex = rightBracketIndex + 1;
                    // return the text before this label
                    // test if there EXIST text before this label
                    if (leftBracketIndex != readIndex)
                    {
                        // return text
                        yield return new UBBContent(ubbText.Substring(readIndex, leftBracketIndex - readIndex));
                    }
                    // return label, include [ and ]
                    yield return new UBBContent(ubbText.Substring(leftBracketIndex, rightBracketIndex - leftBracketIndex + 1), false, factory);
                    // move read index to the next char after right bracket
                    readIndex = rightBracketIndex + 1;
                }
                else
                {
                    // not a label
                    // reset the test index to the position after [
                    testIndex = leftBracketIndex + 1;
                }
            }

            if (readIndex < ubbText.Length)
            {
                // failed to match, return the last content
                yield return new UBBContent(ubbText.Substring(readIndex));
            }
        }

        /// <summary>
        /// Match stragegy
        /// </summary>
        /// <param name="ubbContents"></param>
        /// <returns></returns>
        protected abstract IEnumerable<UBBContent> MatchUBBLabels(IEnumerable<UBBContent> ubbContents);

        /// <summary>
        /// build document
        /// </summary>
        /// <param name="ubbContents"></param>
        /// <returns></returns>
        protected virtual UBBDocument BuildDocument(IEnumerable<UBBContent> ubbContents)
        {
            UBBDocument document = new UBBDocument();
            var navigator = document.CreateNavigator();

            foreach (var ubbContent in ubbContents)
            {
                if (ubbContent.DisplayType == UBBContentType.Text)
                {
                    if (navigator.Current is TextElement)
                    {
                        // for text
                        navigator.AppendAfterAndMoveTo(new TextElement(ubbContent.DisplayText));
                    }
                    else
                    {
                        // for text
                        navigator.AppendChildAndMoveTo(new TextElement(ubbContent.DisplayText));
                    }
                }
                else if (ubbContent.DisplayType == UBBContentType.CloseLabel)
                {
                    // close labal
                    // reservedElements (handle: [b] [i] A [/b] B [/i])
                    var reservedElements = new Stack<UBBElement>();
                    var labelName = ubbContent.GetLabelName();
                    // remove
                    while (true)
                    {
                        if (navigator.Current.LabelName == labelName)
                        {
                            // match
                            if (!navigator.MoveUp())
                                throw new InvalidOperationException("Cannot move up ubb tree");
                            break;
                        }
                        else
                        {
                            // not match
                            // save (skip text element)
                            if (!(navigator.Current is TextElement))
                                reservedElements.Push(navigator.Current);
                            // move up
                            if (!navigator.MoveUp())
                                throw new InvalidOperationException("Cannot move up ubb tree");
                        }
                    }
                    // recover reserved elements
                    while (reservedElements.Count != 0)
                    {
                        var reservedElm = reservedElements.Pop();
                        // clone
                        var clonedElm = reservedElm.Clone() as UBBElement;
                        if (clonedElm == null)
                            throw new InvalidOperationException("Clone ubb element " + reservedElm.ToString() + " failed");
                        // append
                        navigator.AppendChildAndMoveTo(clonedElm);
                    }
                }
                else   // start label
                {
                    // create element
                    if (ubbContent.Factory == null)
                        throw new InvalidOperationException("Factory is null for element text " + ubbContent.DisplayText);
                    var elm = ubbContent.Factory.Create(ubbContent);
                    if (elm == null)
                    {
                        // error
                        throw new InvalidOperationException("Cannot create ubb element for " + ubbContent.DisplayText);
                    }
                    else
                    {
                        while (navigator.Current is TextElement)
                            if (!navigator.MoveUp())
                                break;
                        // add tag
                        navigator.AppendChildAndMoveTo(ubbContent.Create());
                    }
                }
            }

            return document;
        }

        /// <summary>
        /// To check if a text surrounded by [] is a label
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        protected virtual bool IsLabel(string text, out IUBBElementFactory factory)
        {
            factory = null;

            if (string.IsNullOrWhiteSpace(text))
                return false;
            // test max label length strategy
            if (text.Length < 1 || text.Length > MaxLabelLength)
                return false;
            if (text[0] == '/')
                text = text.Substring(1);
            // call tag factory
            return this.ElementFactory.IsLabel(text, out factory);
        }

        protected IUBBElementFactory ElementFactory { get; private set; }

        #endregion protected

        #region private

        private int FindNextLeftBracket(string ubbText, int startIndex)
        {
            for (int i = startIndex; i < ubbText.Length; ++i)
                if (ubbText[i] == '[')
                    return i;
            return -1;
        }

        private int FindNextRightBracket(string ubbText, int startIndex, int maxTestLength)
        {
            for (int i = startIndex; i < ubbText.Length && i - startIndex <= maxTestLength; ++i)
                if (ubbText[i] == ']')
                    return i;
            return -1;
        }

        #endregion private
    }
}